What happens when you perform a query on a type that has subclasses? That's the purpose of this tutorial's transformations. In JPA Tutorial 3 - A Mini Application, we assumed Patrons could only check out books. Now they can checkout Books or DVDs (once we've got two different kinds of resources, adding a third is not a big deal).
It turns out support for inheritance in queries (as well as JPA) is built in. In fact, you do not actually need to do anything other than have one entity inherit from another entity to get everything to work. There are three ways to represent inheritance:
One table for all classes in the hierarchy (the default)
One table for each concrete class
Table with subclasses (this is something that is optional to support in the current JPA spec.)
For now we'll stick with the default setting. Why? Which option you choose will impact performance, the database schema, how normalized your database is, but it will not affect how you write your code.
Step one we need to update our basic system. To do this we'll do the following:
Introduce a new entity type called Resource
Make the book entity inherit from the Resource entity
Move attributes and methods from book that apply to all resources up to the Resource class
Change the BookDao to be a ResourceDao
Re-introduce a stripped down BookDao to support searching by ISBN (which we'll assume apply to books but not DVD's)
Update all the methods that take books and replace them with resources (where appropriate)
Update the methods returning Book and have them instead return Resource (and List<Book> --> List<Resource>)
Update all references to Book and replace them with Resource
Update all the comments that talk about books to talk about resources
You get the idea, it's a lot of work to make this change. That's why we'll do this first and make sure all of our tests pass before we actually add a second kind of resource.
Note the source code for all of these changes is at the bottom of this page.
The Updated Entities
Resource.java
packageentity;importjava.util.Date;importjavax.persistence.CascadeType;importjavax.persistence.Column;importjavax.persistence.Entity;importjavax.persistence.GeneratedValue;importjavax.persistence.Id;importjavax.persistence.OneToOne;importutil.DateTimeUtil;/**
* I represent the base of all things that can be checked of the Library. I hold
* the ID field for all kinds of resources.
*/
@Entitypublicabstractclass Resource {/**
* You do not (cannot in fact) add an id field to any subclasses since they
* already inherit it from me.
*/
@Id
@GeneratedValue
privateLong id;/**
* This was a book attribute, now it is moved up to Resource from Book (and
* remove from Book).
*/
@Column(length = 125, nullable = false)privateString title;/**
* This was a book attribute but since it makes sense for all resources, it
* is now here and inherited by all subclasses of resources.
*
* Now, instead of directly knowing the patron who has borrowed me as in the
* previous version, I now hold on to a Loan object. The loan tracks both
* me, the patron as well as the checkout and due dates.
*/
@OneToOne(mappedBy = "resource",
cascade = CascadeType.PERSIST, optional = true)private Loan loan;public Resource(){}public Resource(finalString title){this.title = title;}publicLong getId(){return id;}publicvoid setId(Long id){this.id = id;}public Loan getLoan(){return loan;}publicvoid setLoan(Loan loan){this.loan = loan;}publicString getTitle(){return title;}publicvoid setTitle(String title){this.title = title;}/**
* Each kind of resource has different rules for how long it can be checked
* out. This abstract method forces each kind of resource to specify its due
* date.
*
* @param checkoutDate
* The date from which to calculate the due date
*
* @return The date the resource is due back
*/publicabstractDate calculateDueDateFrom(finalDate checkoutDate);/**
* Calculate the fine for a given resource based on the number of days late
* the Patron returned it.
*
* @param daysLate
* Number of days this resource is late
*
* @return The fine
*/publicabstractdouble calculateFine(finalint daysLate);/**
* This was in Book but you check in all resources. Notice that this method
* does not know how to calculate the actual fine, so it delegates the fine
* calculation to the derived class. This method is an example of the
* Template Method Pattern.
*
* @param checkinDate
*/publicvoid checkin(finalDate checkinDate){finalDate dueDate = getDueDate();if(getLoan().getDueDate().before(checkinDate)){int daysLate = DateTimeUtil.daysBetween(dueDate, checkinDate);finaldouble fineAmount = calculateFine(daysLate);final Fine f = new Fine(fineAmount, checkinDate, this);
getLoan().getPatron().addFine(f);}
setLoan(null);}publicboolean isOnLoanTo(final Patron p){return(getLoan() == null&& p == null) || getLoan()!= null&& getLoan().getPatron().equals(p);}publicboolean isCheckedOut(){return getLoan()!= null;}publicboolean dueDateEquals(finalDate date){return(date == null&&!isCheckedOut())
|| getLoan().getDueDate().equals(date);}publicDate getDueDate(){if(isCheckedOut()){return getLoan().getDueDate();}returnnull;}}
It turns out that this change touched all of the entities. So here are the rest of the entities changed to reflect this new base class. Book.java
This class lost a lot of its methods as they were moved up to to Resource.
packageentity;importjava.util.Calendar;importjava.util.Date;importjava.util.HashSet;importjava.util.Set;importjavax.persistence.CascadeType;importjavax.persistence.Column;importjavax.persistence.Entity;importjavax.persistence.ManyToMany;importjavax.persistence.NamedQueries;importjavax.persistence.NamedQuery;/**
* I represent a Book. I have one named query to find a book by its isbn number.
* I have a many to many relationship with author. Since I define the mappedBy,
* I'm the (arbitrarily picked) master of the relationship. I also take care of
* cascading changes to the database. I also have One To One relationship with a
* Loan that is optional (so it can be null). If I have a loan, I'm checked out
* and the loan knows the Patron, checkout date and due date.
*/
@Entity/**
* A named query must have a globally unique name. That is why these are named
* "Book."... These queries could be associated with any entity. Given that this
* query deals with books, it seems appropriate to put it here. Named queries
* will probably be pre-compiled. They are available from the entity manager by
* using em.getNamedQueyr("Book.findById").
*/
@NamedQueries({ @NamedQuery(name = "Book.findByIsbn",
query = "SELECT b FROM Book b WHERE b.isbn = :isbn")})publicclassBookextends Resource {
@Column(length = 20, nullable = false)privateString isbn;privateDate printDate;/**
* Authors may have written several books and vice-versa. We had to pick one
* side of this relationship as the primary one and we picked books. It was
* arbitrary but since we're dealing with books, we decided to make this
* side the primary size. The mappedBy connects this relationship to the one
* that is in Author. When we merge or persist, changes to this collection
* and the contents of the collection will be updated. That is, if we update
* the name of the author in the set, when we persist the book, the author
* will also get updated.
*
* Note that if we did not have the cascade setting here, they if we tried
* to persist a book with an unmanaged author (e.g. a newly created one),
* the entity manager would complain of a transient object.
*/
@ManyToMany(mappedBy = "booksWritten", cascade = { CascadeType.PERSIST,
CascadeType.MERGE})privateSet<Author> authors;publicBook(finalString t, finalString i, finalDate printDate,
final Author... authors){super(t);
setIsbn(i);
setPrintDate(printDate);for(Author a : authors){
addAuthor(a);}}publicBook(){}publicSet<Author> getAuthors(){if(authors == null){
authors = newHashSet<Author>();}return authors;}publicvoid setAuthors(finalSet<Author> authors){this.authors = authors;}publicString getIsbn(){return isbn;}publicvoid setIsbn(finalString isbn){this.isbn = isbn;}publicDate getPrintDate(){return printDate;}publicvoid setPrintDate(finalDate printDate){this.printDate = printDate;}publicvoid addAuthor(final Author author){
getAuthors().add(author);}
@Overridepublicboolean equals(finalObject rhs){return rhs instanceofBook&&((Book) rhs).getIsbn().equals(getIsbn());}
@Overridepublicint hashCode(){return getIsbn().hashCode();}publicboolean wasWrittenBy(Author a){return getAuthors().contains(a);}
@OverridepublicDate calculateDueDateFrom(Date checkoutDate){finalCalendar c = Calendar.getInstance();
c.setTime(checkoutDate);
c.add(Calendar.DATE, 14);return c.getTime();}
@Overridepublicdouble calculateFine(finalint daysLate){return .25 * daysLate;}}
Fine.java
We need to change all of Book references to instead be Resource.
packageentity;importjava.util.Date;importjavax.persistence.Entity;importjavax.persistence.GeneratedValue;importjavax.persistence.Id;importjavax.persistence.OneToOne;importjavax.persistence.Temporal;importjavax.persistence.TemporalType;/**
* I represent a single fine assigned to a Patron for a resource returned after its
* due date.
*
* I use new new features of JPA.
*/
@Entitypublicclass Fine {
@Id
@GeneratedValue
privateLong id;privatedouble amount;
@Temporal(TemporalType.DATE)privateDate dateAdded;
@OneToOne
private Resource resource;public Fine(){}public Fine(finaldouble amount, finalDate dateAdded,
final Resource resource){
setAmount(amount);
setDateAdded(dateAdded);
setResource(resource);}public Resource getResource(){return resource;}publicvoid setResource(final Resource resource){this.resource = resource;}publicDate getDateAdded(){return dateAdded;}publicvoid setDateAdded(Date dateAdded){this.dateAdded = dateAdded;}publicdouble getAmount(){return amount;}publicvoid setAmount(double fine){this.amount = fine;}publicLong getId(){return id;}publicvoid setId(Long id){this.id = id;}}
LoanId.java
Change the bookId to resourceId, update the names queries that refer to book to instead refer to Resource.
packageentity;importjava.io.Serializable;/**
* I'm a custom, multi-part key. I represent the key of a Loan object, which
* consists of a Patron id and a Resource id.
*
* These two values together must be unique. The names of my attributes are the
* same as the names used in Loan (patronId, rescoureId).
*
* I also must be Serializable.
*/publicclass LoanId implementsSerializable{privatestaticfinallong serialVersionUID = -2947379879626719748L;/**
* The following two fields must have names that match the names used in the
* Loan class.
*/privateLong patronId;privateLong resourceId;public LoanId(){}public LoanId(finalLong patronId, finalLong resourceId){this.patronId = patronId;this.resourceId = resourceId;}publicLong getResourceId(){return resourceId;}publicLong getPatronId(){return patronId;}
@Overridepublicboolean equals(finalObject rhs){return rhs instanceof LoanId
&&((LoanId) rhs).resourceId.equals(resourceId)&&((LoanId) rhs).patronId.equals(patronId);}
@Overridepublicint hashCode(){return patronId.hashCode()* resourceId.hashCode();}}
Loan.java
Loan refers to resource instead of book. This includes its join columns and named queries.
packageentity;importjava.util.Date;importjavax.persistence.Column;importjavax.persistence.Entity;importjavax.persistence.Id;importjavax.persistence.IdClass;importjavax.persistence.JoinColumn;importjavax.persistence.ManyToOne;importjavax.persistence.NamedQueries;importjavax.persistence.NamedQuery;importjavax.persistence.Temporal;importjavax.persistence.TemporalType;/**
* I'm an entity with a 2-part key. The first part of my key is a Resource id, the
* second is a Patron id.
* <P>
* To make this work, we must do several things: Use the annotation IdClass,
* Specify each of the parts of the id by using Id annotation (2 times in this
* case), Set each Id column to both insertable = false and updatable = false,
* Create an attribute representing the type of the id (Resource, Patron), Use
* JoinColumn with the name = id and insertable = false, updatable = false.
*/
@Entity
@IdClass(LoanId.class)
@NamedQueries({
@NamedQuery(name = "Loan.resourcesLoanedTo",
query = "SELECT l.resource FROM Loan l WHERE l.patron.id = :patronId"),
@NamedQuery(name = "Loan.byResourceId",
query = "SELECT l FROM Loan l WHERE l.resourceId = :resourceId"),
@NamedQuery(name = "Loan.overdueResources",
query = "SELECT l.resource FROM Loan l WHERE l.dueDate < :date"),
@NamedQuery(name = "Loan.patronsWithOverdueResources",
query = "SELECT l.patron FROM Loan l WHERE l.dueDate < :date")})publicclass Loan {/**
* Part 1 of a 2-part Key. The name must match the name in LoanId.
*/
@Id
@Column(name = "resourceId", insertable = false, updatable = false)privateLong resourceId;/**
* Part 2 of a 2-part Key. The name must match the name in LoanId.
*/
@Id
@Column(name = "patronId", insertable = false, updatable = false)privateLong patronId;/**
* A duplicate column in a sense, this one gives us the actual Patron rather
* than just having the id of the Patron.
*
* In the reference material I read, putting in insertable and updatable =
* false did not seem required. However, when using the hibernate entity
* manger I got a null pointer exception and had to step through the source
* code to fix the problem.
*/
@ManyToOne
@JoinColumn(name = "patronId", insertable = false, updatable = false)private Patron patron;/**
* Same comment as for patron attribute above.
*/
@ManyToOne
@JoinColumn(name = "resourceId", insertable = false, updatable = false)private Resource resource;/**
* The date type can represent a date, a time or a time stamp (date and
* time). In our case we just want the date.
*/
@Temporal(TemporalType.DATE)
@Column(updatable = false)privateDate checkoutDate;
@Temporal(TemporalType.DATE)
@Column(updatable = false)privateDate dueDate;public Loan(){}public Loan(final Resource r, final Patron p, finalDate checkoutDate){
setResourceId(r.getId());
setPatronId(p.getId());
setResource(r);
setPatron(p);
setCheckoutDate(checkoutDate);
setDueDate(r.calculateDueDateFrom(checkoutDate));}public Resource getResource(){return resource;}publicvoid setResource(final Resource resource){this.resource = resource;}publicLong getResourceId(){return resourceId;}publicvoid setResourceId(finalLong resourceId){this.resourceId = resourceId;}publicDate getCheckoutDate(){return checkoutDate;}publicvoid setCheckoutDate(finalDate checkoutDate){this.checkoutDate = checkoutDate;}publicDate getDueDate(){return dueDate;}publicvoid setDueDate(finalDate dueDate){this.dueDate = dueDate;}public Patron getPatron(){return patron;}publicvoid setPatron(final Patron patron){this.patron = patron;}publicLong getPatronId(){return patronId;}publicvoid setPatronId(finalLong patronId){this.patronId = patronId;}publicvoid checkin(finalDate checkinDate){
getResource().checkin(checkinDate);
getPatron().checkin(this);}}
Patron.java
The changes to Patron are a little less extreme. Other than some comments referring to books (you can find them), one method changed:
publicvoid checkout(final Resource r, finalDate checkoutDate){final Loan l = new Loan(r, this, checkoutDate);
getCheckedOutResources().add(l);
r.setLoan(l);}
The exceptions
Rename all of the exceptions with "Book" in their name. Replace "Book" with "Resource"
Rename all of the variables names bookId --> resourceId
How can you easliy go about doing this? Use the refactor factor in Eclipse. Select an exception class, right-click and select Refactor:Rename and enter a new name. You can also do the same thing by selecting the attribute name, right-click, refactor:rename and enter the new name. Make sure to select the bottom two selections regarding renaming the getter and setter.
The Dao's
BookDao.java
Most of the functionality that was in BookDao is now in ResourceDao.java. Why is this? Or better yet, why is the a BookDao at all? Look at the one method and answer the question for yourself (or ask).
Library.java
The Library has several changes. First, most references to Book have been replaced with Resource. Second, it now has 4 dao's instead of 3 (BookDao became ResourceDao and there's a new BookDao).
packagesession;importjava.util.Date;importjava.util.List;importjavax.persistence.EntityNotFoundException;importentity.Address;importentity.Author;importentity.Book;importentity.Loan;importentity.Patron;importentity.Resource;importexception.PatronHasFines;importexception.ResourceAlreadyCheckedOut;importexception.ResourceNotCheckedOut;/**
* This class provides a basic facade to the library system. If we had a user
* interface, it would interact with this object rather than dealing with all of
* the underlying Daos.
*/publicclass Library {private ResourceDao resourceDao;private BookDao bookDao;private PatronDao patronDao;private LoanDao loanDao;public ResourceDao getResourceDao(){return resourceDao;}publicvoid setResourceDao(final ResourceDao bookDao){this.resourceDao = bookDao;}public PatronDao getPatronDao(){return patronDao;}publicvoid setPatronDao(final PatronDao patronDao){this.patronDao = patronDao;}public LoanDao getLoanDao(){return loanDao;}publicvoid setLoanDao(final LoanDao loanDao){this.loanDao = loanDao;}public BookDao getBookDao(){return bookDao;}publicvoid setBookDao(BookDao bookDao){this.bookDao = bookDao;}publicBook createBook(finalString title, finalString isbn,
finalDate date, final Author a1, final Author a2){finalBook b = newBook(title, isbn, date, a1, a2);
getResourceDao().create(b);return b;}publicList<Book> findBookByIsbn(String isbn){return getBookDao().findByIsbn(isbn);}public Patron createPatron(finalString patronId, finalString fname,
finalString lname, finalString phoneNumber, final Address a){return getPatronDao().createPatron(fname, lname, phoneNumber, a);}public Patron findPatronById(finalLong id){final Patron p = getPatronDao().retrieve(id);if(p == null){thrownew EntityNotFoundException(String.format("Patron with id: %d does not exist", id));}return p;}public Resource findResourceById(Long id){final Resource r = getResourceDao().findById(id);if(r == null){thrownew EntityNotFoundException(String.format("Book with Id:%d does not exist", id));}return r;}publicvoid returnResource(finalDate checkinDate,
finalLong... resourceIds){for(Long resourceId : resourceIds){final Loan l = getLoanDao().getLoanFor(resourceId);if(l == null){thrownew ResourceNotCheckedOut(resourceId);}
l.checkin(checkinDate);
getLoanDao().remove(l);}}publicvoid checkout(finalLong patronId, finalDate checkoutDate,
finalLong... resourceIds){final Patron p = findPatronById(patronId);double totalFines = p.calculateTotalFines();if(totalFines > 0.0d){thrownew PatronHasFines(totalFines);}for(Long id : resourceIds){final Resource r = findResourceById(id);if(r.isCheckedOut()){thrownew ResourceAlreadyCheckedOut(id);}
p.checkout(r, checkoutDate);}}publicList<Resource> listResourcesOnLoanTo(finalLong patronId){return getLoanDao().listResourcesOnLoanTo(patronId);}publicList<Resource> findAllOverdueResources(finalDate compareDate){return getLoanDao().listAllOverdueResources(compareDate);}publicList<Patron> findAllPatronsWithOverdueBooks(finalDate compareDate){return getLoanDao().listAllPatronsWithOverdueResources(compareDate);}publicdouble calculateTotalFinesFor(finalLong patronId){return getPatronDao().retrieve(patronId).calculateTotalFines();}publicdouble tenderFine(finalLong patronId, double amountTendered){final Patron p = getPatronDao().retrieve(patronId);return p.pay(amountTendered);}}
LoanDao.java
This class no longer knows about books, it only knows about resources.
packagesession;importjava.util.Date;importjava.util.List;importjavax.persistence.NoResultException;importentity.Loan;importentity.Patron;importentity.Resource;/**
* Provide some basic queries focused around the Loan object. These queries
* could have been placed in either the PatronDao or ResourceDao. However,
* neither seemed like quite the right place so we created this new Dao.
*/publicclass LoanDao extends BaseDao {/**
* Given a resource id, find the associated loan or return null if none
* found.
*
* @param resrouceId
* Id of resource on loan
*
* @return Loan object that holds onto resourceId
*/public Loan getLoanFor(Long resourceId){try{return(Loan) getEm().createNamedQuery("Loan.byResourceId")
.setParameter("resourceId", resourceId).getSingleResult();}catch(NoResultException e){returnnull;}}publicvoid remove(final Loan l){
getEm().remove(l);}/**
* Return resources that are due after the compareDate.
*
* @param compareDate
* If a resource's due date is after compareDate, then it is
* included in the list. Note that this named query uses
* projection. Have a look at Loan.java.
*
* @return a list of all the resources that were due after this date.
*/
@SuppressWarnings("unchecked")publicList<Resource> listAllOverdueResources(finalDate compareDate){return getEm().createNamedQuery("Loan.overdueResources").setParameter("date", compareDate).getResultList();}/**
* Essentially the same query as listAllOverdueResources but we return the
* Patrons instead of the resources. This method uses a named query that
* uses projection.
*
* @param compareDate
* If a patron has at least one resources that was due after the
* compare date, include them.
*
* @return A list of the patrons with at least one overdue resources
*/
@SuppressWarnings("unchecked")publicList<Patron> listAllPatronsWithOverdueResources(finalDate compareDate){return getEm().createNamedQuery("Loan.patronsWithOverdueResources")
.setParameter("date", compareDate).getResultList();}/**
* Return all resources on loan to the provided patron id.
*
* @param patronId
* If patron id is invalid, this method will not notice it.
*
* @return Zero or more resources on loan to the patron in question
*/
@SuppressWarnings("unchecked")publicList<Resource> listResourcesOnLoanTo(finalLong patronId){return getEm().createNamedQuery("Loan.resourcesLoanedTo").setParameter("patronId", patronId).getResultList();}}
ResourceDao.java
Many of the BookDao functions are now here and they work with Resources intead of with Books (or rather they work with both but the interface deals with Resources).
packagesession;importentity.Resource;/**
* This class offers the basic create, read, update, delete functions required
* for a resource. As we implement more complex requirements, we'll be coming
* back to this class to add additional queries.
*/publicclass ResourceDao extends BaseDao {publicvoid create(final Resource r){
getEm().persist(r);}public Resource retrieve(finalLong id){return getEm().find(Resource.class, id);}publicvoid remove(Long id){final Resource r = retrieve(id);if(r != null){
getEm().remove(r);}}public Resource update(Resource r){return getEm().merge(r);}public Resource findById(Long id){return getEm().find(Resource.class, id);}}
The Tests
BookDaoTest.java
Removed (or moved to ResourceDaoTest, take your pick).
LibraryTest.java
Updated to work primarily with Resources instead of Books. Also, added additional initialization code since the library now has four dao's instead of 3.
packagesession;importstaticorg.junit.Assert.assertEquals;importstaticorg.junit.Assert.assertFalse;importstaticorg.junit.Assert.assertNotNull;importstaticorg.junit.Assert.assertTrue;importstaticorg.junit.Assert.fail;importjava.util.Calendar;importjava.util.Date;importjava.util.List;importjava.util.Set;importjavax.persistence.EntityNotFoundException;importorg.junit.Before;importorg.junit.BeforeClass;importorg.junit.Test;importutil.DateTimeUtil;importentity.Address;importentity.Author;importentity.Book;importentity.Name;importentity.Patron;importentity.Resource;importexception.InsufficientFunds;importexception.PatronHasFines;importexception.ResourceAlreadyCheckedOut;importexception.ResourceNotCheckedOut;publicclass LibraryTest extends EntityManagerBasedTest {privatestaticfinallong ID_DNE = -443123222l;privatestaticfinalString PATRON_ID = "113322";privatestaticfinalString ISBN = "1-932394-15-X";privatestaticDate CURRENT_DATE;privatestaticDate CURRENT_PLUS_8;privatestaticDate CURRENT_PLUS_14;privatestaticDate CURRENT_PLUS_15;private Library library;
@Before
publicvoid setupLibrary(){final ResourceDao rd = new ResourceDao();
rd.setEm(getEm());final PatronDao pd = new PatronDao();
pd.setEm(getEm());final LoanDao ld = new LoanDao();
ld.setEm(getEm());final BookDao bd = new BookDao();
library = new Library();
library.setResourceDao(rd);
library.setPatronDao(pd);
library.setLoanDao(ld);
library.setBookDao(bd);}
@BeforeClass
publicstaticvoid setupDates(){Calendar c = Calendar.getInstance();
DateTimeUtil.removeTimeFrom(c);
CURRENT_DATE = c.getTime();
c.add(Calendar.DAY_OF_MONTH, 8);
CURRENT_PLUS_8 = c.getTime();
c.add(Calendar.DAY_OF_MONTH, 6);
CURRENT_PLUS_14 = c.getTime();
c.add(Calendar.DAY_OF_MONTH, 1);
CURRENT_PLUS_15 = c.getTime();}
@Test
publicvoid addBook(){finalBook b = createBook();finalSet<Author> authors = b.getAuthors();final Resource found = library.findResourceById(b.getId());
assertTrue(found instanceofBook);
assertTrue(((Book) found).getAuthors().containsAll(authors));}
@Test(expected = EntityNotFoundException.class)publicvoid lookupBookThatDoesNotExist(){
library.findResourceById(ID_DNE);}
@Test
publicvoid addPatron(){final Patron p = createPatron();final Patron found = library.findPatronById(p.getId());
assertNotNull(found);}
@Test(expected = EntityNotFoundException.class)publicvoid lookupPatronThatDoesNotExist(){
library.findPatronById(ID_DNE);}
@Test
publicvoid checkoutBook(){finalBook b1 = createBook();finalBook b2 = createBook();final Patron p = createPatron();
library.checkout(p.getId(), CURRENT_DATE, b1.getId(), b2.getId());finalList<Resource> list = library.listResourcesOnLoanTo(p.getId());
assertEquals(2, list.size());for(Resource r : list){
assertTrue(r.isOnLoanTo(p));
assertTrue(r.dueDateEquals(CURRENT_PLUS_14));}}
@Test
publicvoid returnBook(){finalBook b = createBook();final Patron p = createPatron();
library.checkout(p.getId(), CURRENT_DATE, b.getId());finalint resourcesBefore = p.getCheckedOutResources().size();
assertTrue(b.isCheckedOut());
library.returnResource(CURRENT_PLUS_8, b.getId());
assertEquals(resourcesBefore - 1, p.getCheckedOutResources().size());
assertFalse(b.isCheckedOut());
assertEquals(0, p.getFines().size());}
@Test
publicvoid returnResourceLate(){finalBook b = createBook();final Patron p = createPatron();
library.checkout(p.getId(), CURRENT_DATE, b.getId());
library.returnResource(CURRENT_PLUS_15, b.getId());
assertEquals(1, p.getFines().size());
assertEquals(.25, p.calculateTotalFines());}
@Test(expected = ResourceNotCheckedOut.class)publicvoid returnResourceThatsNotCheckedOut(){finalBook b = createBook();
assertFalse(b.isCheckedOut());
library.returnResource(CURRENT_PLUS_8, b.getId());}
@Test(expected = ResourceAlreadyCheckedOut.class)publicvoid checkoutBookThatIsAlreadyCheckedOut(){finalBook b = createBook();final Patron p1 = createPatron();final Patron p2 = createPatron();
library.checkout(p1.getId(), CURRENT_DATE, b.getId());
library.checkout(p2.getId(), CURRENT_DATE, b.getId());}
@Test(expected = EntityNotFoundException.class)publicvoid checkoutBookThatDoesNotExist(){final Patron p = createPatron();
library.checkout(p.getId(), CURRENT_DATE, ID_DNE);}
@Test(expected = EntityNotFoundException.class)publicvoid checkoutBookToPatronThatDoesNotExist(){finalBook b = createBook();
library.checkout(ID_DNE, CURRENT_DATE, b.getId());}
@Test
publicvoid findOverdueBooks(){final Patron p = createPatron();finalBook b1 = createBook();finalBook b2 = createBook();
library.checkout(p.getId(), CURRENT_DATE, b1.getId());
library.checkout(p.getId(), CURRENT_PLUS_8, b2.getId());finalList<Resource> notOverdue = library
.findAllOverdueResources(CURRENT_PLUS_8);
assertEquals(0, notOverdue.size());finalList<Resource> overdue = library
.findAllOverdueResources(CURRENT_PLUS_15);
assertEquals(1, overdue.size());
assertTrue(overdue.contains(b1));}
@Test
publicvoid patronsWithOverdueBooks(){final Patron p = createPatron();finalBook b1 = createBook();finalBook b2 = createBook();
library.checkout(p.getId(), CURRENT_DATE, b1.getId());
library.checkout(p.getId(), CURRENT_PLUS_8, b2.getId());finalList<Patron> noPatrons = library
.findAllPatronsWithOverdueBooks(CURRENT_PLUS_14);
assertEquals(0, noPatrons.size());finalList<Patron> onePatron = library
.findAllPatronsWithOverdueBooks(CURRENT_PLUS_15);
assertEquals(1, onePatron.size());}
@Test
publicvoid calculateTotalFinesForPatron(){final Patron p = createPatron();finalBook b1 = createBook();finalBook b2 = createBook();
library.checkout(p.getId(), CURRENT_DATE, b1.getId());
library.checkout(p.getId(), CURRENT_DATE, b2.getId());
library.returnResource(CURRENT_PLUS_15, b1.getId(), b2.getId());
assertEquals(.5, library.calculateTotalFinesFor(p.getId()));}
@Test
publicvoid payFineExactAmount(){final Patron p = createPatron();finalBook b1 = createBook();
library.checkout(p.getId(), CURRENT_DATE, b1.getId());
library.returnResource(CURRENT_PLUS_15, b1.getId());double change = library.tenderFine(p.getId(), .25);
assertEquals(0d, change);
assertEquals(0, p.getFines().size());}
@Test(expected = InsufficientFunds.class)publicvoid payFineInsufficientFunds(){final Patron p = createPatron();finalBook b1 = createBook();
library.checkout(p.getId(), CURRENT_DATE, b1.getId());
library.returnResource(CURRENT_PLUS_15, b1.getId());
library.tenderFine(p.getId(), .20);}/**
* This is an example of a test where we expect an exception. However,
* unlike other tests where we use expected=ExceptionClass.class, we need to
* catch the exception because we are additionally verifying a value in the
* thrown exception. This test is written how you'd write a test expecting
* an exception prior to JUnit 4.
*/
@Test
publicvoid patronCannotCheckoutWithFines(){final Patron p = createPatron();finalBook b1 = createBook();
library.checkout(p.getId(), CURRENT_DATE, b1.getId());
library.returnResource(CURRENT_PLUS_15, b1.getId());finalBook b2 = createBook();try{
library.checkout(p.getId(), CURRENT_DATE, b2.getId());
fail(String.format("Should have thrown exception: %s",
PatronHasFines.class.getName()));}catch(PatronHasFines e){
assertEquals(.25, e.getTotalFines());}}privateBook createBook(){final Author a1 = new Author(newName("Christian", "Bauer"));final Author a2 = new Author(newName("Gavin", "King"));return library.createBook("Hibernate In Action", ISBN, Calendar
.getInstance().getTime(), a1, a2);}private Patron createPatron(){final Address a = new Address("5080 Spectrum Drive", "", "Dallas",
"TX", "75001");return library.createPatron(PATRON_ID, "Brett", "Schuchert",
"555-1212", a);}}
PatronDao.test
Only a comment changes in this one. If you use Eclipse's refactoring feature, it automatically gets updated.
ResourceDaoTest.java
The renamed and updated BookDaoTest.java. This class still builds books (they are currently the only kind of concrete subclass of entity).
packagesession;importstaticorg.junit.Assert.assertEquals;importstaticorg.junit.Assert.assertNotNull;importstaticorg.junit.Assert.assertNull;importstaticorg.junit.Assert.assertTrue;importjava.util.Calendar;importorg.junit.Test;importentity.Author;importentity.Book;importentity.Name;importentity.Resource;publicclass ResourceDaoTest extends BaseDbDaoTest {private ResourceDao dao;/**
* By overriding this method, I'm able to provide a dao to the base class,
* which then installs a new entity manager per test method execution. Note
* that my return type is not the same as the base class' version. I return
* BookDao whereas the base class returns BaseDao. Normally an overridden
* method must return the same type. However, it is OK for an overridden
* method to return a different type so long as that different type is a
* subclass of the type returned in the base class. This is called
* covariance.
*
* @see session.BaseDbDaoTest#getDao()
*/
@Overridepublic ResourceDao getDao(){if(dao == null){
dao = new ResourceDao();}return dao;}
@Test
publicvoid createABook(){finalBook b = createABookImpl();final Resource found = getDao().retrieve(b.getId());
assertNotNull(found);}privateBook createABookImpl(){final Author a1 = new Author(newName("Bill", "Burke"));final Author a2 = new Author(newName("Richard", "Monson-Haefel"));finalBook b = newBook("Enterprise JavaBeans 3.0",
"978-0-596-00978-6", Calendar.getInstance().getTime(), a1, a2);
getDao().create(b);return b;}
@Test
publicvoid removeABook(){finalBook b = createABookImpl();
Resource found = getDao().retrieve(b.getId());
assertNotNull(found);
getDao().remove(b.getId());
found = getDao().retrieve(b.getId());
assertNull(found);}
@Test
publicvoid updateABook(){finalBook b = createABookImpl();finalint initialAuthorCount = b.getAuthors().size();
b.addAuthor(new Author(newName("New", "Author")));
getDao().update(b);final Resource found = getDao().retrieve(b.getId());
assertTrue(found instanceofBook);
assertEquals(initialAuthorCount + 1, ((Book) found).getAuthors().size());}
@Test
publicvoid tryToFindBookThatDoesNotExist(){final Resource r = getDao().retrieve(-1123123123l);
assertNull(r);}}
It turns out support for inheritance in queries (as well as JPA) is built in. In fact, you do not actually need to do anything other than have one entity inherit from another entity to get everything to work. There are three ways to represent inheritance:
For now we'll stick with the default setting. Why? Which option you choose will impact performance, the database schema, how normalized your database is, but it will not affect how you write your code.
Step one we need to update our basic system. To do this we'll do the following:
You get the idea, it's a lot of work to make this change. That's why we'll do this first and make sure all of our tests pass before we actually add a second kind of resource.
Note the source code for all of these changes is at the bottom of this page.
The Updated Entities
Resource.javaIt turns out that this change touched all of the entities. So here are the rest of the entities changed to reflect this new base class.
Book.java
This class lost a lot of its methods as they were moved up to to Resource.
Fine.java
We need to change all of Book references to instead be Resource.
LoanId.java
Change the bookId to resourceId, update the names queries that refer to book to instead refer to Resource.
Loan.java
Loan refers to resource instead of book. This includes its join columns and named queries.
Patron.java
The changes to Patron are a little less extreme. Other than some comments referring to books (you can find them), one method changed:
The exceptions
How can you easliy go about doing this? Use the refactor factor in Eclipse. Select an exception class, right-click and select Refactor:Rename and enter a new name. You can also do the same thing by selecting the attribute name, right-click, refactor:rename and enter the new name. Make sure to select the bottom two selections regarding renaming the getter and setter.
The Dao's
BookDao.javaMost of the functionality that was in BookDao is now in ResourceDao.java. Why is this? Or better yet, why is the a BookDao at all? Look at the one method and answer the question for yourself (or ask).
Here's an updated version of BookDao:
Library.java
The Library has several changes. First, most references to Book have been replaced with Resource. Second, it now has 4 dao's instead of 3 (BookDao became ResourceDao and there's a new BookDao).
LoanDao.java
This class no longer knows about books, it only knows about resources.
ResourceDao.java
Many of the BookDao functions are now here and they work with Resources intead of with Books (or rather they work with both but the interface deals with Resources).
The Tests
BookDaoTest.java
Removed (or moved to ResourceDaoTest, take your pick).
LibraryTest.java
Updated to work primarily with Resources instead of Books. Also, added additional initialization code since the library now has four dao's instead of 3.
PatronDao.test
Only a comment changes in this one. If you use Eclipse's refactoring feature, it automatically gets updated.
ResourceDaoTest.java
The renamed and updated BookDaoTest.java. This class still builds books (they are currently the only kind of concrete subclass of entity).
The Source